package redisDB

import (
	"context"
	"errors"
	"gitee.com/ling-bin/go-utils/idCounter"
	"github.com/go-redis/redis/v8"
	"strconv"
	"time"
)

const (
	/*
	-- KEYS[1]: 锁key
	-- ARGV[1]: 锁value,随机字符串
	-- ARGV[2]: 过期时间
	-- 判断锁key持有的value是否等于传入的value
	-- 如果相等说明是再次获取锁并更新获取时间，防止重入时过期
	-- 这里说明是“可重入锁”
	if redis.call("GET", KEYS[1]) == ARGV[1] then
	    -- 设置
	    redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
	    return "OK"
	else
	    -- 锁key.value不等于传入的value则说明是第一次获取锁
	    -- SET key value NX PX timeout : 当key不存在时才设置key的值
	    -- 设置成功会自动返回“OK”，设置失败返回“NULL Bulk Reply”
	    -- 为什么这里要加“NX”呢，因为需要防止把别人的锁给覆盖了
	    return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
	end
	*/
	lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
    redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
    return "OK"
else
    return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
	/*
	-- 释放锁
	-- 不可以释放别人的锁
	if redis.call("GET", KEYS[1]) == ARGV[1] then
	    -- 执行成功返回“1”
	    return redis.call("DEL", KEYS[1])
	else
	    return 0
	end
	*/
	delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end`
)

// A RedisLock is a redis lock.
type RedisLock struct {
	store   redis.Cmdable // redis客户端
	key     string        // 锁key
	id      string        // 锁value，防止锁被别人获取到
}

// NewRedisLock returns a RedisLock.
func NewRedisLock(store redis.Cmdable, key string) *RedisLock {
	return &RedisLock{
		store: store,
		key:   key,
		id: idCounter.NewObjectID().Hex(),  //
	}
}

//GetId 获取锁编号，锁value值
func (rl *RedisLock) GetId() string {
	return rl.id
}

// Lock acquires the lock.
// ttl 锁过期时间，防止死锁
// 加锁 [可重入锁]
func (rl *RedisLock) Lock(ttl time.Duration) (bool, error) {
	// 默认锁过期时间为500ms，防止死锁
	ctx := context.Background()
	resp := rl.store.Eval(ctx, lockCommand, []string{rl.key}, []string{rl.id, strconv.Itoa(int(ttl.Milliseconds()))})
	reply, err := resp.Text()
	if err != nil {
		return false, err
	}
	return reply == "OK", nil
}

// TryLock acquires the lock.
// ttl 锁过期时间，防止死锁,
// wait 等待获取到锁时间，防止死锁
// 有阻塞加锁,直到超时或加锁成功
func (rl *RedisLock) TryLock(ttl time.Duration,wait time.Duration) (bool, error) {
	endTime := time.Now().Add(wait)
	for {
		b, err := rl.Lock(ttl)
		if err == nil && b {
			return true, nil
		}
		if time.Now().Sub(endTime) >= 0 {
			return false, errors.New("wait Overtime")
		}
		time.Sleep(time.Millisecond)
	}
}

// ULock releases the lock.
// 释放锁
func (rl *RedisLock) ULock() (bool, error) {
	ctx := context.Background()
	resp := rl.store.Eval(ctx, delCommand, []string{rl.key}, []string{rl.id})
	if resp.Err() != nil {
		return false, resp.Err()
	}
	reply, err := resp.Int64()
	if err != nil {
		return false, err
	}
	return reply == 1, nil
}